##############
# (*) Kaleidoscope
# By: Allan Spale
# May 2008

from bsddb import db
import sys
import uuid

from Kaleidoscope.KSColumn import KSColumn, KSColumnError
from Kaleidoscope.KSColumnAdapted import KSColumnAdapted
from Kaleidoscope.KSMappedMixInVirtualType import KSMappedMixInVirtualType
from Kaleidoscope.KSEventManager import KSEventManagerError

class KSColumnMappedError(Exception):
    """
    Exception class for KSColumnMapped

    """
    # REDO ALL CONSTANT PROPERTIES TO BE NORMAL/WRITABLE OUTSIDE OF CLASS...
    # FIX THIS LATER

    B_UNKNOWN_EXCEPTION = True
    B_CLASS_EXCEPTION = False

    B_PRIMARY_COLUMN = True
    B_SECONDARY_COLUMN = False

    def __init__(self, u_message):
        self.__u_message = u_message
    def __str__(self):
        return repr(self.__u_message)

    @staticmethod
    def print_and_raise_error(u_message, b_unknown_exception):
        """
        Convenience method for printing and raising an error for this
        particular Exception subclass.

        """
        #sys.stderr.write(StringIO.StringIO(sys.exc_info()))
        # write initial error
        u_message = u'KSColumnMapped.py: ' + u_message
        sys.stderr.write(u_message + u'\n')

        if (b_unknown_exception):
            raise Exception(u_message)
        else:
            raise KSColumnMappedError(u_message)


class KSColumnMapped(KSColumnAdapted):
    """
    The purpose of this class is to associate an existing databases
    and map the values of keys to their respective database. The semantics
    of creating "indices" on values will be handled by this class. This
    includes handling cascading deletes, changes in values, etc. Typical
    operation: wrap the lower level function call so as to retrieve the
    appropriate values (and not just the IDs).

    Could have made this class a subclass of KSColumn, but it just seemed
    to be better to duplicate the code. This is especially true with the
    slightly different function parameters passed to similarly named functions.

    To keep things simple, use the TransactionFactory of the primary column.
    This implies that the secondary column must be stored in a similar
    datastore as the key column. In this way, if a parent transaction is
    passed to a function, there will not be a conflict in the specific
    subclass of KSTransaction.

    """
    @property
    def is_open(self):
        return self.__b_open

    def __init__(self, ksc_key_column, ksc_secondary_column):
        try:
            self.__b_open = False

            if (isinstance(ksc_key_column, KSColumn)):
                self.__ksc_key_column = ksc_key_column
                self.get_transaction = self.__ksc_key_column.get_transaction
                self.__id = uuid.uuid4()
            else:
                KSColumnError.print_and_raise_error_no_event(u'The primary key '
                    + u'column must be of type KSColumn.',
                    KSColumnError.B_CLASS_EXCEPTION)

            if (isinstance(ksc_secondary_column, KSColumn)):
                self.__ksc_secondary_column = ksc_secondary_column
            elif (isinstance(ksc_secondary_column, unicode)):
                # Create the mixin to override functions that would
                # not handle virtual types properly
                self.__ksc_secondary_column = ksc_secondary_column
                KSColumnMapped.__bases__ += (KSMappedMixInVirtualType,)
            else:
                KSColumnError.print_and_raise_error_no_event(u'The '
                    + u'secondary column must either be a subclass of KSColumn '
                    + u'or of unicode string that corresponds to a valid '
                    + u'virtual type.', KSColumnError.B_CLASS_EXCEPTION)

            # Not useful right now, but necessary
            #self.register_secondary_column_events()

        except db.DBError:
            KSColumnError.print_and_raise_error_no_event(
                u'Flags for the database must be stored in an integer.',
                KSColumnError.B_CLASS_EXCEPTION)


    def __del__(self):
        """
        Upon deletion of the class instance, close the column files.

        """
        self.close()


    # MANUALLY OPEN AND CLOSE FILES BECAUSE OF COLUMN REUSE IN OTHER CLASSES
    def open(self, i_flags, i_permissions, kstx_parent_transaction):
        """
        Open the database files for the columns.

        """
        try:
            if not (self.__b_open):
                kstx_transaction = self.get_transaction(kstx_parent_transaction,
                    self.open)
                self.__ksc_key_column.open(
                    i_flags, i_permissions, kstx_transaction)
                self.__ksc_secondary_column.open(
                    i_flags, i_permissions, kstx_transaction)
                kstx_transaction.commit(0, self.open)
                self.__b_open = True
        except Exception:
            kstx_transaction.cancel()
            raise

        return True


    def close(self):
        """
        Close the database files for the columns.

        """
        try:
            if (self.__b_open):
                self.__ksc_key_column.close()
                self.__ksc_secondary_column.close()
                self.__b_open = False
        except Exception:
            raise

        return True


    def get_data_from_key(self, obj_key, kstx_parent_transaction,
                              b_packed_data):
        """
        Get the data from the primary database based on the key. Using the
        secondary database, take the secondary key stored as the data item in
        the primary database and retrieve the data item from the secondary
        database.

        """
        try:
            kstx_transaction = self.get_transaction(kstx_parent_transaction,
                self.get_data_from_key)
            b_packed_data = None   # ignore

            #print 'mapped: '
            #print self.print_all(kstx_transaction)
            obj_data = self.__ksc_key_column.get_data_from_key(
                obj_key, kstx_transaction, KSColumn.B_REGULAR_DATA)

            if (isinstance(obj_data, list)):
                return_data = []
                for key in obj_data:
                    return_data.append(
                        self.__ksc_secondary_column.get_data_from_key(
                            key, kstx_transaction, b_packed_data))
            else:
                return_data = self.__ksc_secondary_column.get_data_from_key(
                    obj_data, kstx_transaction, b_packed_data)

            kstx_transaction.commit(0, self.get_data_from_key)
            return return_data

        except Exception:
            kstx_transaction.cancel()
            raise


    def get_keys_from_data_value(self, obj_target_data, kstx_parent_transaction,
                                    b_return_packed_data):
        """
        Retrieve the keys that are bound to obj_target_data.

        """
        try:

            kstx_transaction = self.get_transaction(kstx_parent_transaction,
                self.get_keys_from_data_value)
            print 'mapped: '
            print self.print_all(kstx_transaction)

            result = self.__wrapped_get_keys_from_data_value(
                obj_target_data, kstx_transaction, b_return_packed_data)
            kstx_transaction.commit(0, self.get_keys_from_data_value)
            return result

        except Exception:
            kstx_transaction.cancel()
            raise


    def get_keys_from_data_object_range(self, obj_start_data, obj_end_data,
            i_low_comparison_operator, i_high_comparison_operator,
            kstx_parent_transaction, b_return_packed_data):
        """
        Go through the data and find the list of keys from the given values
        in the range of data items. It is assumed that the caller will use
        a type that would support a range of data. Range is
        obj_start_data <= data_item <= obj_end_data. Assume that the data
        objects correspond to data objects from the secondary database.
        Retrieve their respective keys and then pass those to the primary
        database as data objects in order to retrieve the keys from the
        primary database.

        """
        try:
            kstx_transaction = self.get_transaction(kstx_parent_transaction,
                self.get_keys_from_data_object_range)
            list_secondary_keys = \
                self.__ksc_secondary_column.get_keys_from_data_object_range(
                    obj_start_data, obj_end_data,
                    i_low_comparison_operator, i_high_comparison_operator,
                    kstx_transaction, b_return_packed_data)

            list_primary_keys = []
            for item in list_secondary_keys:
                for key in self.__ksc_key_column.get_keys_from_data_value(
                    item, kstx_transaction, b_return_packed_data):
                        list_primary_keys.append(key)

            kstx_transaction.commit(0, self.get_keys_from_data_object_range)
            return list_primary_keys

        except Exception:
            kstx_transaction.cancel()
            raise


    def get_item_from_partial_key(self, obj_partial_key,
                                      kstx_parent_transaction):
        """
        Find the data items closest to matching the partial key. This
        function is more appropriate for keys that are unicode strings.
        The key corresponds to the primary database. The values which are
        returned from this key will correspond to keys from the secondary
        database. Using these secondary keys, the actual data items will be
        returned.

        """
        try:
            kstx_transaction = self.get_transaction(kstx_parent_transaction,
                self.get_item_from_partial_key)
            obj_secondary_key = \
                self.__ksc_key_column.get_item_from_partial_key(
                    obj_partial_key, kstx_transaction, KSColumn.B_REGULAR_DATA)

            if (isinstance(obj_secondary_key, list)):
                return_data = []
                for key in obj_secondary_key:
                    return_data.append(
                        self.__ksc_secondary_column.get_data_from_key(
                            key, kstx_transaction, KSColumn.B_REGULAR_DATA))
            else:
                return_data = self.__ksc_secondary_column.get_data_from_key(
                    key, kstx_transaction, KSColumn.B_REGULAR_DATA)

            kstx_transaction.commit(0, self.get_item_from_partial_key)
            return return_data

        except Exception:
            kstx_transaction.cancel()
            raise


    def has_key(self, obj_key, kstx_parent_transaction):
        """
        Return a boolean value that indicates whether or not the key exists
        in the database.

        """
        try:
            kstx_transaction = self.get_transaction(kstx_parent_transaction,
                self.has_key)
            b_result = self.__ksc_key_column.has_key(
                obj_key, kstx_parent_transaction)
            kstx_transaction.commit(0, self.has_key)
            return b_result

        except Exception:
            kstx_transaction.cancel()
            KSColumnError.print_and_raise_error_no_event(u'Checking for '
                + u'the key could not be completed due to an unknown error.',
                KSColumnError.B_UNKNOWN_EXCEPTION)


    def has_value(self, obj_value, kstx_parent_transaction):
        """
        Return a boolean value that indicates whether or not the value exists
        at least once in the database.

        """
        try:
            kstx_transaction = self.get_transaction(kstx_parent_transaction,
                self.has_value)
            b_result = self.__ksc_secondary_column.has_key(
                obj_value, kstx_transaction)
            kstx_transaction.commit(0, self.has_value)
            return b_result

        except Exception:
            kstx_transaction.cancel()
            KSColumnError.print_and_raise_error_no_event(u'Checking for '
                + u'the value could not be completed due to an unknown error.',
                KSColumnError.B_UNKNOWN_EXCEPTION)


    def insert_item_with_key_value_pair(self, obj_key, obj_data,
                                            kstx_parent_transaction):
        """
        Insert the key/data pair. The key will be stored in the primary
        database. The data value will be checked in the secondary database
        for a unique key. This secondary key will then be stored as the
        data item for the primary database.

        """
        # Look for the key corresponding to the data item in the secondary
        # database. If it does not exist, raise an error. If a duplicate
        # exists, raise an error.
        try:
            kstx_transaction = self.get_transaction(kstx_parent_transaction,
                self.insert_item_with_key_value_pair)
            list_secondary_keys = \
                self.__ksc_secondary_column.get_keys_from_data_value(
                    obj_data, kstx_transaction, KSColumn.B_REGULAR_DATA)

            if (len(list_secondary_keys) == 1):
                self.__ksc_key_column.insert_item_with_key_value_pair(
                    obj_key, list_secondary_keys[0], kstx_transaction)
                kstx_transaction.commit(
                    0, self.insert_item_with_key_value_pair)
            else:
                kstx_transaction.cancel()
                KSColumnError.print_and_raise_error_no_event(u'The data '
                    + u'from the secondary database must have a single key '
                    + u'correspond to it.',
                    KSColumnError.B_CLASS_EXCEPTION)

        except Exception:
            kstx_parent_transaction.cancel()
            raise

        return True


    def insert_value_and_generate_key(self, obj_data, kstx_parent_transaction):
        """
        Insert data with time_id as a key. The key will be stored in the
        primary database. The data value will be checked in the secondary
        database for a unique key. This secondary key will then be stored as
        the data item for the primary database.

        """
        try:
            kstx_transaction = self.get_transaction(kstx_parent_transaction,
                self.insert_value_and_generate_key)
            list_secondary_keys = \
                self.__ksc_secondary_column.get_keys_from_data_value(
                    obj_data, kstx_transaction, KSColumn.B_REGULAR_DATA)

            if (len(list_secondary_keys) == 1):
                self.__ksc_key_column.insert_value_and_generate_key(
                    list_secondary_keys[0], kstx_transaction)
                kstx_transaction.commit(0, self.insert_value_and_generate_key)
                return True
            else:
                kstx_transaction.cancel()
                KSColumnError.print_and_raise_error_no_event(u'The data '
                    + u'from the  secondary database must have a single key '
                    + u'correspond to it.', KSColumnError.B_CLASS_EXCEPTION)

        except Exception:
            kstx_transaction.cancel()
            raise


    def insert_items_with_key_and_value_list(self, obj_key, list_values,
                                                 kstx_parent_transaction):
        """
        Insert the key/data list pair. The key will be stored in the primary
        database. The data values will be checked in the secondary database
        for each one's unique key. The secondary keys will then be stored as the
        data items for the primary database.

        """
        # Look for the key corresponding to the data item in the secondary
        # database. If it does not exist, raise an error. If a duplicate
        # exists, raise an error.
        try:
            kstx_transaction = self.get_transaction(kstx_parent_transaction,
                self.insert_item_with_key_value_pair)

            list_all_secondary_keys = []

            for obj_data in list_values:
                list_secondary_keys = \
                    self.__ksc_secondary_column.get_keys_from_data_value(
                        obj_data, kstx_transaction, KSColumn.B_REGULAR_DATA)

                if (len(list_secondary_keys) == 1):
                    list_all_secondary_keys.append(list_secondary_keys[0])
                else:
                    kstx_transaction.cancel()
                    KSColumnError.print_and_raise_error_no_event(u'The '
                        + u'data from the secondary database must have a '
                        + u'single key correspond to it.',
                        KSColumnError.B_CLASS_EXCEPTION)

                self.__ksc_key_column.insert_items_with_key_and_value_list(
                    obj_key, list_all_secondary_keys, kstx_transaction)
                kstx_transaction.commit(
                    0, self.insert_item_with_key_value_pair)
                return True

        except Exception:
            kstx_parent_transaction.cancel()
            raise


    def change_key(self, obj_old_key, obj_new_key, kstx_parent_transaction):
        """
        Get the data list from the old key, and store it as a new key
        value list.

        """
        try:
            kstx_transaction = self.get_transaction(kstx_parent_transaction,
                self.change_key)
            self.__ksc_key_column.change_key(
                obj_old_key, obj_new_key, kstx_parent_transaction)
            kstx_transaction.commit(0, self.change_key)
            return True

        except Exception:
            kstx_transaction.cancel()
            raise


    def change_all_data_values(self, obj_old_data_value, obj_new_data_value,
                                   kstx_parent_transaction):
        """
        Change all data values to another value. Slow way: scan the database
        and swap out the old values with the new values.

        """
        try:
            kstx_transaction = self.get_transaction(kstx_parent_transaction,
                self.change_all_data_values)

            # Make sure that the new data value exists in the
            # secondary database.
            list_keys = self.__ksc_secondary_column.get_keys_from_data_value(
                obj_new_data_value, kstx_transaction, KSColumn.B_REGULAR_DATA)

            if (len(list_keys) == 1):
                # Now find the secondary key of the old data value for
                # replacement
                secondary_key_value_new = list_keys[0]

                list_keys = \
                    self.__ksc_secondary_column.get_keys_from_data_value(
                        obj_old_data_value, kstx_transaction,
                        KSColumn.B_REGULAR_DATA)

                if (len(list_keys) == 1):
                    secondary_key_value_old = list_keys[0]
                    self.__ksc_key_column.change_all_data_values(
                        secondary_key_value_old, secondary_key_value_new,
                        kstx_transaction)
                    kstx_transaction.commit(0, self.change_all_data_values)
                    return True
                else:
                    kstx_transaction.cancel()
                    KSColumnError.print_and_raise_error_no_event(u'The new data '
                        + u'item that will replace by the new data was not '
                        + u'found in the secondary database.',
                        KSColumnError.B_CLASS_EXCEPTION)
            else:
                kstx_transaction.cancel()
                KSColumnError.print_and_raise_error_no_event(u'The old data item '
                    + u'that will be replaced by the new data was not found '
                    + u'in the secondary database.',
                    KSColumnError.B_CLASS_EXCEPTION)

        except Exception:
            kstx_transaction.cancel()
            raise


    def change_key_value_pair(self, obj_key, obj_data, kstx_parent_transaction):
        """
        Change the data for the given key. Check to make sure that the value
        listed as the data exists as a key in the secondary table.

        """
        try:
            kstx_transaction = self.get_transaction(kstx_parent_transaction,
                self.change_key_value_pair)

            list_secondary_keys = \
                self.__ksc_secondary_column.get_keys_from_data_value(
                    obj_data, kstx_transaction, KSColumn.B_REGULAR_DATA)

            if (len(list_secondary_keys) == 1):
                self.__ksc_key_column.change_key_value_pair(
                    obj_key, list_secondary_keys[0], kstx_transaction)
                kstx_transaction.commit(0, self.change_key_value_pair)
            else:
                kstx_transaction.cancel()
                KSColumnError.print_and_raise_error_no_event(u'The '
                    + u'data from the secondary database must have a '
                    + u'single key correspond to it.',
                    KSColumnError.B_CLASS_EXCEPTION)

            return True

        except Exception:
            kstx_parent_transaction.cancel()
            raise


    def delete_item_by_key(self, obj_key, kstx_parent_transaction):
        """
        Delete the item represented by the given key.

        """
        try:
            kstx_transaction = self.get_transaction(kstx_parent_transaction,
                self.delete_item_by_key)
            self.__ksc_key_column.delete_item_by_key(
                obj_key, kstx_transaction)
            kstx_transaction.commit(0, self.delete_item_by_key)
            return True
        except Exception:
            kstx_transaction.cancel()
            raise


    def delete_items_by_value_list(self, list_values, kstx_parent_transaction):
        """
        Delete any items having a certain data value. Slow way:
        scan the database and swap out the old values with the new values.

        """
        try:
            kstx_transaction = self.get_transaction(kstx_parent_transaction,
                self.delete_items_by_value_list)
            list_secondary_keys = []
            list_temp_keys = []

            for value in list_values:
                list_temp_keys = \
                    self.__ksc_secondary_column.get_keys_from_data_value(
                        value, kstx_transaction, KSColumn.B_REGULAR_DATA)

                for secondary_key in list_temp_keys:
                    list_secondary_keys.append(secondary_key)
                # end loop
            # end loop

            self.__ksc_key_column.delete_items_by_value_list(
                list_secondary_keys, kstx_transaction)
            kstx_transaction.commit(0, self.delete_items_by_value_list)
            return True

        except Exception:
            kstx_transaction.cancel()
            raise


    def get_all(self, kstx_parent_transaction, b_return_packed_data):
        """
        Retrieve entire database of items, both keys and values. Use the data
        items in the primary database as keys for the secondary database to
        retrieve the actual data items.

        """
        try:
            kstx_transaction = self.get_transaction(kstx_parent_transaction,
                self.get_all)
            dict_primary = \
                self.__ksc_key_column.get_all(
                    kstx_transaction, b_return_packed_data)
            for key in dict_primary:
                dict_primary[key] = \
                    self.__ksc_secondary_column.get_data_from_key(
                        dict_primary[key], kstx_transaction,
                        b_return_packed_data)
            # end loop

            kstx_transaction.commit(0, self.get_all)
            return dict_primary

        except Exception:
            kstx_transaction.cancel()
            raise


    def values(self, kstx_parent_transaction, b_return_packed_data):
        """
        Retrieve all values from the database of items. Use the data
        items in the primary database as keys for the secondary database to
        retrieve the actual data items.

        """
        try:
            kstx_transaction = self.get_transaction(kstx_parent_transaction,
                self.values)
            list_values = []

            list_primary = \
                self.__ksc_key_column.values(
                    kstx_transaction, KSColumn.B_REGULAR_DATA)

            if (b_return_packed_data):
                for value in list_primary:
                    list_values.append(
                        self.__ksc_secondary_column.get_data_from_key(
                            value, kstx_transaction, KSColumn.B_PACKED_DATA))
            else:
                for value in list_primary:
                    list_values.append(
                        self.__ksc_secondary_column.get_data_from_key(
                            value, kstx_transaction, KSColumn.B_REGULAR_DATA))
            # end if

            kstx_transaction.commit(0, self.values)
            return list_values

        except Exception:
            kstx_transaction.cancel()
            raise


    def keys(self, kstx_parent_transaction, b_return_packed_data):
        """
        Retrieve all keys from the database of items.

        """
        try:
            kstx_transaction = self.get_transaction(kstx_parent_transaction,
                self.keys)
            keys = self.__ksc_key_column.keys(
                kstx_parent_transaction, b_return_packed_data)
            kstx_transaction.commit(0, self.keys)
            return keys

        except Exception:
            kstx_transaction.cancel()
            raise


    def size(self, kstx_parent_transaction):
        """
        Retrieve the number of rows for this column based on the number of
        keys present in the primary column.

        """
        try:
            kstx_transaction = self.get_transaction(kstx_parent_transaction,
                self.size)
            rows = self.__ksc_key_column.size(kstx_transaction)
            kstx_transaction.commit(0, self.size)
            return rows

        except Exception:
            kstx_transaction.cancel()
            raise


    def print_all(self, kstx_parent_transaction):
        """
        Display all of the data in both the primary and secondary columns.

        """
        try:
            kstx_transaction = self.get_transaction(kstx_parent_transaction,
                self.print_all)
            dict_primary = self.__ksc_key_column.get_all(
                kstx_transaction, KSColumn.B_REGULAR_DATA)

            for item in dict_primary:
                print item, self.__ksc_secondary_column.get_data_from_key(
                    dict_primary[item], kstx_transaction,
                    KSColumn.B_REGULAR_DATA)

            kstx_transaction.commit(0, self.print_all)
            return True

        except Exception:
            kstx_transaction.cancel()
            raise


    def reorganize_keys(self, kstx_parent_transaction):
        """
        Since the primary key column can reorganize the keys in the same way
        as its underlying column class, just call that method relating to
        the column. As for the secondary column, it only points to the actual
        values in the referent column. The actions in the primary column are
        not affecting the values in the secondary column. Therefore, no action
        needs to be taken.

        """
        try:
            kstx_transaction = self.get_transaction(kstx_parent_transaction,
                self.reorganize_keys)
            self.__ksc_key_column.reorganize_keys(kstx_transaction)
            kstx_transaction.commit(0, self.reorganize_keys)
            return True
        except Exception:
            kstx_transaction.cancel()
            KSColumnError.print_and_raise_error_no_event(u'The keys could not '
                + u'be reorganized due to an internal error.',
                KSColumnError.B_UNKNOWN_EXCEPTION)


    def return_value_type(self, kstx_parent_transaction):
        """
        Return the data type of the stored value in the secondary column. This
        may be somewhat pointless if binary strings of data are stored. The
        pickling procedure for storing values should retrain the original
        object type.

        """
        try:
            kstx_transaction = self.get_transaction(kstx_parent_transaction,
                self.return_value_type)

            type_obj = self.__ksc_secondary_column.return_value_type(
                kstx_transaction)

            kstx_transaction.commit(0, self.return_value_type)
            return type_obj

        except Exception:
            kstx_transaction.cancel()
            KSColumnError.print_and_raise_error_no_event(u'The type of the '
                + u'value for the secondary column could not be retrieved.',
                KSColumnError.B_UNKNOWN_EXCEPTION)


    # These functions are going to pass on specific calls to the underlying
    # KSColumn subclasses to register for their events. Since this column
    # is just an extension of those functions, use the same events.

    def register_before_function_event(self, b_is_primary_column,
                                            st_function_name, fn_callback):
        """
        Register a function callback that is called when the event tied to
        another function is first called but does not execute.

        """
        try:
            if (b_is_primary_column):
                return self.__ksc_key_column.ksem_event_manager.\
                    register_before_function_event(
                        st_function_name, fn_callback)
            else:
                return self.__ksc_secondary_column.ksem_event_manager.\
                    register_before_function_event(
                        st_function_name, fn_callback)
        except KSEventManagerError:
            KSColumnError.print_and_raise_error_no_event(u'The "before event" '
                + u'function could not be registered.',
                KSColumnError.B_UNKNOWN_EXCEPTION)


    def delete_before_function_event(self, b_is_primary_column,
                                            st_function_name):
        """
        Un-register from executing the previously registered function callback
        when a function tied to an event is called but has not begun its own
        execution.

        """
        try:
            if (b_is_primary_column):
                return self.__ksc_key_column.ksem_event_manager.\
                    delete_before_function_event(st_function_name)
            else:
                return self.__ksc_secondary_column.ksem_event_manager.\
                    delete_before_function_event(st_function_name)
        except KSEventManagerError:
            KSColumnError.print_and_raise_error_no_event(u'The "before event" '
                + u'function could not be removed.',
                KSColumnError.B_UNKNOWN_EXCEPTION)


    def register_after_function_event(self, b_is_primary_column,
                                            st_function_name, fn_callback):
        """
        Register a function callback that is called when the event tied to
        another function completes its execution.

        """
        try:
            if (b_is_primary_column):
                return self.__ksc_key_column.ksem_event_manager.\
                    register_after_function_event(
                        st_function_name, fn_callback)
            else:
                return self.__ksc_secondary_column.ksem_event_manager.\
                    register_after_function_event(
                        st_function_name, fn_callback)
        except KSEventManagerError:
            KSColumnError.print_and_raise_error_no_event(u'The "after event" '
                + u'function could not be registered.',
                KSColumnError.B_UNKNOWN_EXCEPTION)


    def delete_after_function_event(self, b_is_primary_column,
                                        st_function_name):
        """
        Un-register from executing the previously registered function callback
        when a function tied to an event is called and completed its own
        execution.

        """
        try:
            if (b_is_primary_column):
                return self.__ksc_key_column.ksem_event_manager.\
                    delete_after_function_event(st_function_name)
            else:
                return self.__ksc_secondary_column.ksem_event_manager.\
                    delete_after_function_event(st_function_name)
        except KSEventManagerError:
            KSColumnError.print_and_raise_error_no_event(u'The "after event" '
                + u'function could not be removed.',
                KSColumnError.B_UNKNOWN_EXCEPTION)


    def register_secondary_column_events(self):
        """
        Whenever columns mapped to the secondary column are operated upon,
        register function callbacks in order to properly modify the existing
        secondary column.

        """
        if (isinstance(self.__ksc_secondary_column, KSColumn)):
            # Register for events that occur to secondary_column:
            # all key changes and key deletes
            self.__ksc_secondary_column.ksem_event_manager.\
                register_after_function_event(u'change_key',
                    self.__event_change_key)
            self.__ksc_secondary_column.ksem_event_manager.\
                register_after_function_event(u'change_all_data_values',
                    self.__event_change_all_data_values)
            self.__ksc_secondary_column.ksem_event_manager.\
                register_after_function_event(u'change_key_value_pair',
                    self.__event_change_key_value_pair)
            self.__ksc_secondary_column.ksem_event_manager.\
                register_after_function_event(u'delete_item_by_key',
                    self.__event_delete_item_by_key)
            self.__ksc_secondary_column.ksem_event_manager.\
                register_after_function_event(u'delete_items_by_value_list',
                    self.__event_delete_items_by_value_list)
            self.__ksc_secondary_column.ksem_event_manager.\
                register_after_function_event(u'reorganize_keys',
                    self.__event_reorganize_keys)
        else:
            KSColumnError.print_and_raise_error_no_event(u'The secondary data '
                + u'column must be of type KSColumn.',
                KSColumnError.B_CLASS_EXCEPTION)


    def generate_id(self):
        """
        Create a unique column ID. Do not send an event because this is a
        trivial function that does not need to be announced.

        """
        try:
            return self.__ksc_key_column.generate_id()

        except Exception:
            KSColumnError.print_and_raise_error(u'An error occurred while '
                + u'trying to generate a new column ID.',
                KSColumnError.B_CLASS_EXCEPTION)


    def __wrapped_get_keys_from_data_value(self, obj_target_data,
                            kstx_parent_transaction, b_return_packed_data):
        """
        Go through the data and find the list of keys from the given values.
        Assume that the data objects correspond to data objects from the
        secondary database. Retrieve their respective keys and then pass
        those to the primary database as data objects in order to retrieve the
        keys from the primary database.

        """
        try:
            kstx_transaction = self.get_transaction(kstx_parent_transaction,
                self.__wrapped_get_keys_from_data_value)
            list_secondary_keys = \
                self.__ksc_secondary_column.get_keys_from_data_value(
                    obj_target_data, kstx_transaction, b_return_packed_data)

            list_primary_keys = []
            for item in list_secondary_keys:
                for key in self.__ksc_key_column.get_keys_from_data_value(
                    item, kstx_transaction, b_return_packed_data):
                        list_primary_keys.append(key)

            kstx_transaction.commit(0, self.__wrapped_get_keys_from_data_value)
            return list_primary_keys

        except Exception:
            kstx_transaction.cancel()
            raise


    # CALLBACKS: (format-- self.__uuid_owner, i_event_id, list_data)
    def __event_change_key(self, *args):
        """
        Change the value of the keys in the primary column.

        """
        try:
            kstx_transaction = self.get_transaction(None,
                self.__event_change_key)
            list_data = args[2]
            obj_old_key, obj_new_key = list_data

            self.__ksc_key_column.change_all_data_values(
                obj_old_key, obj_new_key, kstx_transaction)

            kstx_transaction.commit(0, self.__event_change_key)

        except Exception:
            kstx_transaction.cancel()
            KSColumnError.print_and_raise_error_no_event(u'The "change key" '
                + u'event could not be processed due to an internal error.',
                KSColumnError.B_UNKNOWN_EXCEPTION)


    def __event_change_all_data_values(self, *args):
        """
        These values will have to be changed to NULL because it is difficult
        to determine the intent of how to adjust to these changes.

        """
        try:
            kstx_transaction = self.get_transaction(None,
                self.__event_change_all_data_values)
            list_data = args[2]
            list_changed_keys = list_data[3]

            for obj_key in list_changed_keys:
                self.__ksc_key_column.change_all_data_values(
                    obj_key, KSColumn.NULL_VALUE, kstx_transaction)

            kstx_transaction.commit(0, self.__event_change_all_data_values)

        except Exception:
            KSColumnError.print_and_raise_error_no_event(u'The "change all '
                + u'data values" event could not be processed due to '
                + u'an internal error.',
                KSColumnError.B_UNKNOWN_EXCEPTION)


    def __event_change_key_value_pair(self, *args):
        """
        This value will have to be changed to NULL because it is difficult
        to determine the intent of how to adjust to these changes.

        """
        try:
            kstx_transaction = self.get_transaction(None,
                self.__event_change_key_value_pair)
            list_data = args[2]
            obj_key = list_data[0]

            self.__ksc_key_column.change_all_data_values(
                obj_key, KSColumn.NULL_VALUE, kstx_transaction)

            kstx_transaction.commit(0, self.__event_change_key_value_pair)

        except Exception:
            KSColumnError.print_and_raise_error_no_event(u'The "change key '
                + u'value pair" event could not be processed due to '
                + u'an internal error.',
                KSColumnError.B_UNKNOWN_EXCEPTION)


    def __event_delete_item_by_key(self, *args):
        """
        Set the deleted item to NULL.

        """
        try:
            kstx_transaction = self.get_transaction(None,
                self.__event_delete_item_by_key)
            list_data = args[2]
            obj_key = list_data[0]

            self.__ksc_key_column.change_all_data_values(
                obj_key, KSColumn.NULL_VALUE, kstx_transaction)

            kstx_transaction.commit(0, self.__event_delete_item_by_key)

        except Exception:
            KSColumnError.print_and_raise_error_no_event(u'The "delete item '
                + u'by key" event could not be processed due to '
                + u'an internal error.',
                KSColumnError.B_UNKNOWN_EXCEPTION)


    def __event_delete_items_by_value_list(self, *args):
        """
        Set the deleted item to NULL.

        """
        try:
            kstx_transaction = self.get_transaction(None,
                self.__event_delete_items_by_value_list)
            list_data = args[2]
            list_deleted_keys = list_data[1]

            for obj_key in list_deleted_keys:
                self.__ksc_key_column.change_all_data_values(
                    obj_key, KSColumn.NULL_VALUE, kstx_transaction)

            kstx_transaction.commit(0, self.__event_delete_items_by_value_list)

        except Exception:
            KSColumnError.print_and_raise_error_no_event(u'The "delete items '
                + u'by value list" event could not be processed due to '
                + u'an internal error.',
                KSColumnError.B_UNKNOWN_EXCEPTION)


    def __event_reorganize_keys(self, *args):
        """
        Change to the new key value.

        """
        try:
            kstx_transaction = self.get_transaction(None,
                self.__event_reorganize_keys)
            list_data = args[2]
            obj_old_key, obj_new_key = list_data

            self.__ksc_key_column.change_all_data_values(
                obj_old_key, obj_new_key, kstx_transaction)

            kstx_transaction.commit(0, self.__event_reorganize_keys)

        except Exception:
            KSColumnError.print_and_raise_error_no_event(u'The "reorganize '
                + u'keys" event could not be processed due to an internal '
                + u'error.', KSColumnError.B_UNKNOWN_EXCEPTION)